﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Reflection;
using System.Web.UI.WebControls;
using System.Globalization;
using System.ComponentModel;

namespace NLBS.WebControls.UI
{
    internal class ClientObjectSerializer
    {
        private const string emptyObject = "{}";
        private const string emptyArray = "[]";

        internal static string Serialize(Type baseType, object value)
        {
            return SerializeObject(baseType, value);
        }

        internal static string Serialize(object value)
        {
            if (value == null)
            {
                return "\"\"";
            }

            if (value is string || value is Unit)
            {
                return String.Format(@"""{0}""", QuoteJScriptString(value.ToString()));
            }

            else if (value is long
                        || value is int
                        || value is double
                        || value is byte)
            {
                return Convert.ToString(value, CultureInfo.InvariantCulture);
            }

            else if (value is bool)
            {
                return value.ToString().ToLower();
            }

            else if (value is System.TimeSpan)
            {
                TimeSpan timeSpan = (TimeSpan)value;
                return string.Format(
                                "[{0}, {1}, {2}, {3}, {4}]",
                                timeSpan.Days,
                                timeSpan.Hours,
                                timeSpan.Minutes,
                                timeSpan.Seconds,
                                timeSpan.Milliseconds);
            }

            else if (value is Enum)
            {
                return Convert.ToInt32(value).ToString();
            }

            else if (value is Hashtable)
            {
                return SerializeHashtable((Hashtable)value);
            }

            else if (value is IList)
            {
                return SerializeIList((IList)value);
            }

            else if (value is DateTime)
            {
                return SerializeDateTime(value);
            }

            return SerializeObject(value);
        }

        internal static string SerializeHashtable(Hashtable value)
        {
            ArrayList members = new ArrayList();
            ICollection keys = value.Keys;
            foreach (string key in keys)
            {
                members.Add(JSONPair(key, Serialize(value[key])));
            }
            return ToJSON(members);
        }

        internal static string SerializeHashtable(IList value, string idPropertyName)
        {
            ArrayList members = new ArrayList();
            if (value.Count <= 0)
            {
                return emptyObject;
            }

            ArrayList serializableProperties = GetSerializableProperties(value[0]);

            PropertyDescriptor idProperty = null;
            foreach (object member in value)
            {
                if (idProperty == null)
                {
                    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(member, true);
                    idProperty = properties.Find(idPropertyName, true);
                }

                string serialized = SerializeObject(member, serializableProperties);
                if (serialized == emptyObject) continue;
                members.Add(JSONPair(idProperty.GetValue(member), serialized));
            }

            return ToJSON(members);
        }

        internal static string SerializeHashtable(IList value, Type baseType, string idPropertyName)
        {
            ArrayList members = new ArrayList();
            if (value.Count <= 0)
            {
                return emptyObject;
            }

            ArrayList serializableProperties = GetSerializableProperties(value[0]);

            PropertyDescriptor idProperty = null;
            foreach (object member in value)
            {
                if (idProperty == null)
                {
                    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(member, true);
                    idProperty = properties.Find(idPropertyName, true);
                }

                string serialized = SerializeObject(baseType, member, serializableProperties);
                if (serialized == emptyObject) continue;
                members.Add(JSONPair(idProperty.GetValue(member), serialized));
            }

            return ToJSON(members);
        }

        private static ArrayList GetSerializableProperties(object p)
        {
            PropertyInfo[] props = p.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
            ArrayList propertyNames = new ArrayList();
            foreach (PropertyInfo info in props)
            {
                if (!info.CanRead)
                {
                    continue;
                }

                if (Attribute.IsDefined(info, typeof(ClientSerializableAttribute)))
                {
                    ClientSerializableInfo clientSerializableInfo = new ClientSerializableInfo();
                    clientSerializableInfo.PropertyName = info.Name;

                    ClientSerializableAttribute serializableAttribute = GetClientSerializableAttribute(info);
                    if (serializableAttribute.PropertyName != string.Empty)
                    {
                        clientSerializableInfo.SerializablePropertyName = serializableAttribute.PropertyName;
                    }
                    else
                    {
                        clientSerializableInfo.SerializablePropertyName = info.Name;
                    }
                    clientSerializableInfo.RenderIfDefault = serializableAttribute.RenderDefault;

                    DefaultValueAttribute[] customAttributes = (DefaultValueAttribute[])
                        info.GetCustomAttributes(typeof(DefaultValueAttribute), false);

                    if (customAttributes.Length > 0)
                    {
                        clientSerializableInfo.DefaultValue = customAttributes[0].Value;
                    }

                    propertyNames.Add(clientSerializableInfo);
                }
            }

            return propertyNames;
        }

        public class ClientSerializableInfo
        {
            public string PropertyName;
            public object DefaultValue;
            public bool RenderIfDefault;
            public string SerializablePropertyName;
        }

        private static string ToJSON(ArrayList members)
        {
            return String.Format("{{{0}}}", Join(members));
        }

        private static string JSONPair(object name, string value)
        {
            return String.Format("\"{0}\":{1}", name, value);
        }

        private static string SerializeIList(IList value)
        {
            ArrayList properties = new ArrayList();
            if (value.Count <= 0)
            {
                return emptyArray;
            }
            foreach (object o in value)
            {
                properties.Add(Serialize(o));
            }

            return String.Format("[{0}]", Join(properties));
        }

        private static string SerializeDateTime(object value)
        {
            return string.Format("[{0}]", ((DateTime)value).ToString("yyyy,M,d,H,m,s", DateTimeFormatInfo.InvariantInfo));
        }

        private static string Join(ArrayList list)
        {
            return String.Join(",", (string[])list.ToArray(typeof(string)));
        }

        private static string SerializeObject(object source)
        {
            return SerializeObject(source.GetType(), source);
        }

        private static string SerializeObject(object source, ArrayList propertiesToSerialize)
        {
            if (source == null) return emptyObject;
            Type type = source.GetType();
            BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic;
            PropertyInfo[] props = type.GetProperties(flags);

            if (props.Length == 0)
            {
                return emptyObject;
            }

            ArrayList propertyDefinition = new ArrayList();

            foreach (ClientSerializableInfo serializableInfo in propertiesToSerialize)
            {
                PropertyInfo info = type.GetProperty(serializableInfo.PropertyName, flags);

                object propertyValue = info.GetValue(source, new object[0]);
                if (!serializableInfo.RenderIfDefault)
                {
                    if (propertyValue == null && serializableInfo.DefaultValue == null) continue;
                    if (propertyValue.Equals(serializableInfo.DefaultValue)) continue;
                }

                string value = Serialize(propertyValue);
                if (value != emptyObject)
                {
                    propertyDefinition.Add(JSONPair(serializableInfo.SerializablePropertyName, value));
                }
            }

            return ToJSON(propertyDefinition);
        }

        private static string SerializeObject(Type type, object source, ArrayList propertiesToSerialize)
        {
            if (source == null) return emptyObject;

            BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic;
            PropertyInfo[] props = type.GetProperties(flags);

            if (props.Length == 0)
            {
                return emptyObject;
            }

            ArrayList propertyDefinition = new ArrayList();

            foreach (ClientSerializableInfo serializableInfo in propertiesToSerialize)
            {
                PropertyInfo info = type.GetProperty(serializableInfo.PropertyName, flags);

                object propertyValue = info.GetValue(source, new object[0]);
                if (!serializableInfo.RenderIfDefault)
                {
                    if (propertyValue == null && serializableInfo.DefaultValue == null) continue;
                    if (propertyValue.Equals(serializableInfo.DefaultValue)) continue;
                }

                string value = Serialize(propertyValue);
                if (value != emptyObject)
                {
                    propertyDefinition.Add(JSONPair(serializableInfo.SerializablePropertyName, value));
                }
            }

            return ToJSON(propertyDefinition);
        }

        private static string SerializeObject(Type baseType, object source)
        {
            if (source == null) return emptyObject;
            PropertyInfo[] props = baseType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);

            if (props.Length == 0)
            {
                return emptyObject;
            }

            ArrayList propertyDefinition = new ArrayList();

            foreach (PropertyInfo info in props)
            {
                if (HasToBeAdded(info, source))
                {
                    object propertyValue = info.GetValue(source, new object[0]);
                    string propertyName = info.Name;

                    ClientSerializableAttribute serializableAttribute = GetClientSerializableAttribute(info);
                    if (serializableAttribute != null && serializableAttribute.PropertyName != string.Empty)
                    {
                        propertyName = serializableAttribute.PropertyName;
                    }

                    string value = Serialize(propertyValue);
                    if (value != emptyObject)
                    {
                        propertyDefinition.Add(JSONPair(propertyName, value));
                    }
                }
            }

            return ToJSON(propertyDefinition);
        }

        private static bool CheckDefaultValue(PropertyInfo info, object source)
        {
            ClientSerializableAttribute serializableAttribute = GetClientSerializableAttribute(info);
            if (serializableAttribute.RenderDefault)
            {
                return true;
            }

            object[] customAttributes = info.GetCustomAttributes(typeof(DefaultValueAttribute), false);

            if (customAttributes.Length == 0)
            {
                return true;
            }

            object propertyValue = info.GetValue(source, new object[0]);

            object defaultValue = ((DefaultValueAttribute)customAttributes[0]).Value;

            if (defaultValue == null)
            {
                return propertyValue != null;
            }

            return !defaultValue.Equals(propertyValue);

        }

        private static bool HasToBeAdded(PropertyInfo info, object source)
        {
            if (!Attribute.IsDefined(info, typeof(ClientSerializableAttribute)))
            {
                return false;
            }

            return CheckDefaultValue(info, source);
        }

        private static ClientSerializableAttribute GetClientSerializableAttribute(PropertyInfo info)
        {
            return (ClientSerializableAttribute)info.GetCustomAttributes(typeof(ClientSerializableAttribute), false)[0];
        }

        internal static string QuoteJScriptString(string value)
        {
            StringBuilder jsBuilder = new StringBuilder(value.Length);
            for (int i = 0; i < value.Length; i++)
            {
                char currentChar = value[i];
                if (currentChar <= '"')
                {
                    switch (currentChar)
                    {
                        case '\t':
                            {
                                jsBuilder.Append(@"\t");
                                break;
                            }
                        case '\n':
                            {
                                jsBuilder.Append(@"\n");
                                break;
                            }
                        case '\r':
                            {
                                jsBuilder.Append(@"\r");
                                break;
                            }
                        case '"':
                            {
                                jsBuilder.Append("\\\"");
                                break;
                            }
                        default:
                            {
                                jsBuilder.Append(value[i]);
                                break;
                            }
                    }
                }
                else
                {
                    if (currentChar == '\'')
                    {
                        jsBuilder.Append("'");
                    }
                    else if (currentChar == '\\')
                    {
                        jsBuilder.Append(@"\\");
                    }
                    else
                    {
                        jsBuilder.Append(value[i]);
                    }
                }
            }
            return jsBuilder.ToString();
        }
    }
}
